'use strict';

var _ = require('lodash');
var async = require('async');
var constants = require('../helpers/constants.js');
var crypto = require('crypto');
var extend = require('extend');
var OrderBy = require('../helpers/orderBy.js');
const accounts = require('../helpers/accounts');
var sandboxHelper = require('../helpers/sandbox.js');
var schema = require('../schema/transactions.js');
var sql = require('../sql/transactions.js');
var TransactionPool = require('../logic/transactionPool.js');
var transactionTypes = require('../helpers/transactionTypes.js');
const { preparePaging } = require('../helpers/pagination.js');
var Transfer = require('../logic/transfer.js');

// Private fields
var __private = {};
var shared = {};
var modules;
var library;
var self;

__private.assetTypes = {};

/**
 * Initializes library with scope content and generates a Transfer instance
 * and a TransactionPool instance.
 * Calls logic.transaction.attachAssetType().
 * @memberof module:transactions
 * @class
 * @classdesc Main transactions methods.
 * @param {function} cb - Callback function.
 * @param {scope} scope - App instance.
 * @return {setImmediateCallback} Callback function with `self` as data.
 */
// Constructor
function Transactions (cb, scope) {
  library = {
    logger: scope.logger,
    db: scope.db,
    schema: scope.schema,
    ed: scope.ed,
    balancesSequence: scope.balancesSequence,
    logic: {
      transaction: scope.logic.transaction
    },
    genesisblock: scope.genesisblock
  };

  self = this;

  __private.transactionPool = new TransactionPool(
      scope.config.broadcasts.broadcastInterval,
      scope.config.broadcasts.releaseLimit,
      scope.config.transactions.maxTxsPerQueue,
      scope.logic.transaction,
      scope.bus,
      scope.logger
  );

  __private.assetTypes[transactionTypes.SEND] = library.logic.transaction.attachAssetType(
      transactionTypes.SEND, new Transfer()
  );

  setImmediate(cb, null, self);
}

// Private methods
/**
 * Counts totals and gets transaction list from `trs_list` view.
 * @private
 * @param {Object} filter
 * @param {function} cb - Callback function.
 * @return {setImmediateCallback} error | data: {transactions, count}
 */
__private.list = function (filter, cb) {
  var params = {};
  var where = [];
  var allowedFieldsMap = {
    blockId: '"t_blockId" = ${blockId}',
    senderPublicKey: '"t_senderPublicKey" = DECODE (${senderPublicKey}, \'hex\')',
    recipientPublicKey: '"m_recipientPublicKey" = DECODE (${recipientPublicKey}, \'hex\')',
    senderId: '"t_senderId" = ${senderId}',
    recipientId: '"t_recipientId" = ${recipientId}',
    inId: '("t_recipientId" = ${inId} OR "t_senderId" = ${inId})',
    isIn: '("t_recipientId" = ${isIn} OR "t_senderId" = ${isIn})',
    fromHeight: '"b_height" >= ${fromHeight}',
    toHeight: '"b_height" <= ${toHeight}',
    fromTimestamp: '"t_timestamp" >= ${fromTimestamp}',
    toTimestamp: '"t_timestamp" <= ${toTimestamp}',
    senderIds: '"t_senderId" IN (${senderIds:csv})',
    recipientIds: '"t_recipientId" IN (${recipientIds:csv})',
    senderPublicKeys: 'ENCODE ("t_senderPublicKey", \'hex\') IN (${senderPublicKeys:csv})',
    recipientPublicKeys: 'ENCODE ("m_recipientPublicKey", \'hex\') IN (${recipientPublicKeys:csv})',
    minAmount: '"t_amount" >= ${minAmount}',
    maxAmount: '"t_amount" <= ${maxAmount}',
    minFee: '"t_fee" >= ${minFee}',
    maxFee: '"t_fee" <= ${maxFee}',
    type: '"t_type" = ${type}',
    types: '"t_type" IN (${types:csv})',
    noClutter: '("t_amount" > 0 OR "t_type" < 8)',
    minConfirmations: 'confirmations >= ${minConfirmations}',
    limit: null,
    offset: null,
    orderBy: null,
    returnAsset: null,
    returnUnconfirmed: null,
    // FIXME: Backward compatibility, should be removed after transitional period
    ownerAddress: null,
    ownerPublicKey: null
  };
  var owner = '';
  var isFirstWhere = true;

  var processParams = function (value, key) {
    var field = String(key).split(':');
    if (field.length === 1) {
      // Only field identifier, so using default 'OR' condition
      field.unshift('OR');
    } else if (field.length === 2) {
      // Condition supplied, checking if correct one
      if (_.includes(['or', 'and'], field[0].toLowerCase())) {
        field[0] = field[0].toUpperCase();
      } else {
        throw new Error('Incorrect condition [' + field[0] + '] for field: ' + field[1]);
      }
    } else {
      // Invalid parameter 'x:y:z'
      throw new Error('Invalid parameter supplied: ' + key);
    }

    // Mutating parameters when unix timestamp is supplied
    if (_.includes(['fromUnixTime', 'toUnixTime'], field[1])) {
      // ADAMANT epoch is 1464109200 as unix timestamp
      value = value - constants.epochTime.getTime() / 1000;
      field[1] = field[1].replace('UnixTime', 'Timestamp');
    }

    if (!_.includes(_.keys(allowedFieldsMap), field[1])) {
      throw new Error('Parameter is not supported: ' + field[1]);
    }

    // Checking for empty parameters, 0 is allowed for few
    if (!value && !(value === 0 && _.includes(['fromTimestamp', 'minAmount', 'minConfirmations', 'type', 'offset', 'minFee'], field[1]))) {
      throw new Error('Value for parameter [' + field[1] + '] cannot be empty');
    }

    if (allowedFieldsMap[field[1]]) {
      where.push((!isFirstWhere ? (field[0] + ' ') : '') + allowedFieldsMap[field[1]]);
      params[field[1]] = value;
      isFirstWhere = false;
    }
  };

  // Generate list of fields with conditions
  try {
    _.each(filter, processParams);
  } catch (err) {
    return setImmediate(cb, err.message);
  }

  // FIXME: Backward compatibility, should be removed after transitional period
  if (filter.ownerAddress && filter.ownerPublicKey) {
    owner = '("t_senderPublicKey" = DECODE (${ownerPublicKey}, \'hex\') OR "t_recipientId" = ${ownerAddress})';
    params.ownerPublicKey = filter.ownerPublicKey;
    params.ownerAddress = filter.ownerAddress;
  }

  if (!filter.limit) {
    params.limit = 100;
  } else {
    params.limit = Math.abs(filter.limit);
  }

  if (!filter.offset) {
    params.offset = 0;
  } else {
    params.offset = Math.abs(filter.offset);
  }

  if (params.limit > 1000) {
    return setImmediate(cb, 'Invalid limit, maximum is 1000');
  }

  var orderBy = OrderBy(
    filter.orderBy, {
        sortField: 'timestamp',
        sortMethod: 'DESC',
        sortFields: sql.sortFields,
        sortField: 'timestamp',
        sortMethod: 'DESC',
        fieldPrefix: function (sortField) {
          if (['height'].indexOf(sortField) > -1) {
            return 'b_' + sortField;
          } else if (['confirmations'].indexOf(sortField) > -1) {
            return sortField;
          } else {
            return 't_' + sortField;
          }
        }
      }
  );

  if (orderBy.error) {
    return setImmediate(cb, orderBy.error);
  }

  let unconfirmedTransactions = [];

  if (filter.returnUnconfirmed) {
    const allowedFilters = [
      'blockId',
      'fromHeight',
      'toHeight',
      'minAmount',
      'maxAmount',
      'senderId',
      'senderIds',
      'recipientId',
      'recipientIds',
      'senderPublicKey',
      'senderPublicKeys',
      'recipientPublicKey',
      'recipientPublicKeys',
      'inId',
      'isIn',
      'type',
      'types',
    ];

    unconfirmedTransactions = modules.transactions.getUnconfirmedTransactions(filter, {
      allowedFilters,
      defaultCondition: 'OR',
    });

    const paging = preparePaging(params, unconfirmedTransactions.length);

    params.offset = paging.db.offset;
    params.limit  = paging.db.limit;

    params.mergingOffset = paging.merge.offset;
    params.mergingLimit  = paging.merge.limit;
  }

  library.db.query(sql.countList({
    where: where,
    owner: owner
  }), params).then(function (rows) {
    var count = rows.length ? rows[0].count : 0;
    var sql_method = 'list';
    if (filter.returnAsset) {
      sql_method = 'listFull';
    }
    library.db.query(sql[sql_method]({
      where: where,
      owner: owner,
      sortField: orderBy.sortField,
      sortMethod: orderBy.sortMethod
    }), params).then(function (rows) {
      let transactions = [];

      for (var i = 0; i < rows.length; i++) {
        transactions.push(library.logic.transaction.dbRead(rows[i]));
      }

      if (filter.returnUnconfirmed) {
        count += unconfirmedTransactions.length;

        transactions = modules.transactions.mergeUnconfirmedTransactions(
          transactions,
          unconfirmedTransactions,
          {
            orderBy,
            limit: params.mergingLimit,
            offset: params.mergingOffset,
            returnAsset: filter.returnAsset,
          }
        );
      }

      var data = {
        transactions: transactions,
        count: count + unconfirmedTransactions.length
      };

      return setImmediate(cb, null, data);
    }).catch(function (err) {
      library.logger.error(err.stack);
      return setImmediate(cb, 'Transactions#list error');
    });
  }).catch(function (err) {
    library.logger.error(err.stack);
    return setImmediate(cb, 'Transactions#list error');
  });
};

/**
 * Gets transaction by id from `trs_list` view.
 * @private
 * @param {string} id
 * @param {function} cb - Callback function.
 * @return {setImmediateCallback} error | data: {transaction}
 */
__private.getById = function (id, cb) {
  library.db.query(sql.getById, { id: id }).then(function (rows) {
    if (!rows.length) {
      return setImmediate(cb, 'Transaction not found: ' + id);
    }

    var transacton = library.logic.transaction.dbRead(rows[0]);

    return setImmediate(cb, null, transacton);
  }).catch(function (err) {
    library.logger.error(err.stack);
    return setImmediate(cb, 'Transactions#getById error');
  });
};

/**
 * Gets transaction by id from `trs_list_full` view.
 * @private
 * @param {string} id
 * @param {function} cb - Callback function.
 * @return {setImmediateCallback} error | data: {transaction}
 */
__private.getByIdFullAsset = function (id, cb) {
  library.db.query(sql.getByIdFull, { id: id }).then(function (rows) {
    if (!rows.length) {
      return setImmediate(cb, 'Transaction not found: ' + id);
    }

    var transacton = library.logic.transaction.dbRead(rows[0]);

    return setImmediate(cb, null, transacton);
  }).catch(function (err) {
    library.logger.error(err.stack);
    return setImmediate(cb, 'Transactions#getById error');
  });
};
/**
 * Gets votes by transaction id from `votes` table.
 * @private
 * @param {transaction} transaction
 * @param {function} cb - Callback function.
 * @return {setImmediateCallback} error | data: {added, deleted}
 */
__private.getVotesById = function (transaction, cb) {
  library.db.query(sql.getVotesById, { id: transaction.id }).then(function (rows) {
    if (!rows.length) {
      return setImmediate(cb, 'Transaction not found: ' + transaction.id);
    }

    var votes = rows[0].votes.split(',');
    var added = [];
    var deleted = [];

    for (var i = 0; i < votes.length; i++) {
      if (votes[i].substring(0, 1) === '+') {
        added.push(votes[i].substring(1));
      } else if (votes[i].substring(0, 1) === '-') {
        deleted.push(votes[i].substring(1));
      }
    }

    transaction.votes = { added: added, deleted: deleted };

    return setImmediate(cb, null, transaction);
  }).catch(function (err) {
    library.logger.error(err.stack);
    return setImmediate(cb, 'Transactions#getVotesById error');
  });
};

/**
 * Gets transaction by calling parameter method.
 * @private
 * @param {Object} method
 * @param {Object} req
 * @param {function} cb - Callback function.
 * @return {setImmediateCallback} error | data: {transaction}
 */
__private.getPooledTransaction = function (method, req, cb) {
  library.schema.validate(req.body, schema.getPooledTransaction, function (err) {
    if (err) {
      return setImmediate(cb, err[0].message);
    }

    var transaction = self[method](req.body.id);

    if (!transaction) {
      return setImmediate(cb, 'Transaction not found');
    }

    transaction.blockId = null;
    transaction.height = null;
    transaction.confirmations = 0;

    return setImmediate(cb, null, { transaction: transaction });
  });
};

/**
 * Gets transactions by calling parameter method.
 * Filters by senderPublicKey or address if they are present.
 * @private
 * @param {Object} method
 * @param {Object} req
 * @param {function} cb - Callback function.
 * @return {setImmediateCallback} error | data: {transactions, count}
 */
__private.getPooledTransactions = function (method, req, cb) {
  library.schema.validate(req.body, schema.getPooledTransactions, function (err) {
    if (err) {
      return setImmediate(cb, err[0].message);
    }

    var transactions = self[method](true);
    var i, toSend = [];

    if (req.body.senderPublicKey || req.body.address) {
      for (i = 0; i < transactions.length; i++) {
        if (transactions[i].senderPublicKey === req.body.senderPublicKey || transactions[i].recipientId === req.body.address) {
          toSend.push(transactions[i]);
        }
      }
    } else {
      for (i = 0; i < transactions.length; i++) {
        toSend.push(transactions[i]);
      }
    }

    toSend = toSend.map((transaction) => ({
      ...transaction,
      blockId: null,
      height: null,
      confirmations: 0,
    }));

    return setImmediate(cb, null, { transactions: toSend, count: transactions.length });
  });
};

// Public methods
/**
 * Check if transaction is in pool
 * @param {string} id
 * @return {function} Calls transactionPool.transactionInPool
 */
Transactions.prototype.transactionInPool = function (id) {
  return __private.transactionPool.transactionInPool(id);
};

/**
 * @param {string} id
 * @return {function} Calls transactionPool.getUnconfirmedTransaction
 */
Transactions.prototype.getUnconfirmedTransaction = function (id) {
  return __private.transactionPool.getUnconfirmedTransaction(id);
};


/**
 * Merge unconfirmed transactions in a sorted array
 *
 * @param {Array<Transaction>} targetArray Sorted array to merge unconfirmed transactions into
 * @param {Array<UnconfirmedTransaction>} unconfirmedTransactions Array of unconfirmed transactions to merge
 * @param {Object} options
 * @param {Object} [options.orderBy] - Options for transaction ordering. See `helpers/orderBy.js`
 * @param {number} [options.limit] - Maximum number of transactions in the final array
 * @param {number} [options.offset] - Offset for the final array
 * @param {number} [options.returnAsset=1] - Whether to remove assets from all transactions. Default is 1
 * @param {number} [options.withoutDirectTransfers=0] - Whether to remove all transfer transactions. Default is 0
 */
Transactions.prototype.mergeUnconfirmedTransactions = function (
  targetArray,
  unconfirmedTransactions,
  options = {},
) {
  const {
    orderBy,
    limit,
    includeDirectTransfers = 1,
    returnAsset = 1,
    offset = 0,
  } = options;
  const { originalField: sortField, sortMethod } = orderBy;

  const compare = (a, b) => {
    const aField = a[sortField] ? a[sortField] : Infinity;
    const bField = b[sortField] ? b[sortField] : Infinity;

    if (aField > bField) {
      return sortMethod.toUpperCase() === 'DESC' ? -1 : 1;
    }

    if (aField < bField) {
      return sortMethod.toUpperCase() === 'DESC' ? 1 : -1;
    }

    return 0;
  };

  const mergedArray = [];

  const sortedUnconfirmedTransactions = unconfirmedTransactions.sort(compare);

  let i = 0;
  let j = 0;

  while (i < targetArray.length && j < sortedUnconfirmedTransactions.length) {
    if (compare(targetArray[i], sortedUnconfirmedTransactions[j]) <= 0) {
      mergedArray.push(targetArray[i++]);
    } else {
      mergedArray.push(sortedUnconfirmedTransactions[j++]);
    }
  }

  while (i < targetArray.length) {
    mergedArray.push(targetArray[i++]);
  }
  while (j < sortedUnconfirmedTransactions.length) {
    mergedArray.push(sortedUnconfirmedTransactions[j++]);
  }

  let result = mergedArray;

  if (!includeDirectTransfers) {
    result = result.filter((transaction) => (
      transaction.type !== transactionTypes.SEND
    ));
  }

  result = result.slice(offset, limit ? offset + limit : undefined)

  if (!returnAsset) {
    result = result.map((transaction) => ({
      ...transaction,
      asset: undefined,
    }));
  }

  return result;
}

/**
 * Retrieves unconfirmed transactions based on the provided filter and options
 *
 * @param {Object} filter - Criteria to filter unconfirmed transactions
 * @param {Object} [options]
 * @param {string[]} [options.allowedFilters=[]] - List of keys allowed for filtering transactions
 * @param {Object} [options.aliases={}] - Key-value pairs for aliasing filter keys
 * @param {string} [options.defaultCondition='AND'] - Default logical condition for combining filters ('AND' or 'OR')
 * @returns {Object[]} - Array of unconfirmed transactions matching the criteria
 * @throws {Error} - If `filter` is not an object
 */
Transactions.prototype.getUnconfirmedTransactions = function (filter, options = {}) {
  const {
    allowedFilters = [],
    aliases = {},
    defaultCondition = 'AND',
    important = {},
  } = options;

  let transactions = this.getUnconfirmedTransactionList();

  if (filter?.constructor !== Object) {
    throw new Error('filter should be of type "object"');
  }

  if (JSON.stringify(filter) === '{}') {
    return transactions;
  }

  if ('isIn' in filter && 'inId' in filter) {
    // prioritize isIn as api/chats/get does
    delete filter.inId;
  }

  transactions = transactions.filter((transaction) => {
    const matches = {
      assetStateType: (value) => transaction.asset?.state?.type === value,
      assetChatType: (value) => transaction.asset?.chat?.type === value,
      type: (value) => transaction.type === value,
      minAmount: (value) => transaction.amount >= value,
      maxAmount: (value) => transaction.amount <= value,
      senderId: (value) => transaction.senderId === value,
      recipientId: (value) => transaction.recipientId === value,
      inId: (value) => transaction.senderId === value || transaction.recipientId === value,
      isIn: (value) => transaction.senderId === value || transaction.recipientId === value,
      senderPublicKey: (value) => transaction.senderPublicKey === value,
      recipientPublicKey: (value) => {
        return accounts.getAddressByPublicKey(value) === transaction.recipientId;
      },
      fromTimestamp: (value) => transaction.timestamp >= value,
      toTimestamp: (value) => transaction.timestamp <= value,
      types: (value) => value?.includes(transaction.type),
      senderIds: (value) => value?.includes(transaction.senderId),
      recipientIds: (value) => value?.includes(transaction.recipientId),
      senderPublicKeys: (value) => value?.includes(transaction.senderPublicKey),
      recipientPublicKeys: (value) => value?.map(accounts.getAddressByPublicKey).includes(transaction.recipientId),
      key: (value) => transaction.asset?.state?.key === value,
      keyIds: (value) => value?.includes(transaction.asset?.state?.key),
    };

    // Ignore boolean logic for endpoint related filters
    for (const [key, value] of Object.entries(important)) {
      if (!matches[key]?.(value)) {
        return false;
      }
    }

    // Returns empty array if any of the filters are included in the filter
    const exclusiveKeys = ['blockId', 'toHeight'];

    const evaluate = (key, value) => matches[key] ? matches[key](value) : true;

    let result = true;
    let isFirst = true;

    for (const [key, value] of Object.entries(filter)) {
      const upperCaseKey = key.toUpperCase();

      const isOr = upperCaseKey.startsWith('OR:') || (!upperCaseKey.startsWith('AND:') && defaultCondition !== 'AND');

      const actualKey = key.replace(/^(AND:|OR:)/i, "");
      if (exclusiveKeys.includes(actualKey)) {
        return false;
      }

      if (!allowedFilters.includes(actualKey) && allowedFilters.length !== 0) {
        continue;
      }

      const condition = evaluate(aliases[actualKey] ?? actualKey, value);

      if (isFirst && isOr) {
        result = condition;
      } else {
        result = isOr ? result || condition : result && condition ;
      }

      isFirst = false;
    }

    return result;
  });

  return transactions.map((transaction) => ({
    ...transaction,
    blockId: null,
    height: null,
    confirmations: 0,
  }));
}

/**
 * @param {string} id
 * @return {function} Calls transactionPool.getQueuedTransaction
 */
Transactions.prototype.getQueuedTransaction = function (id) {
  return __private.transactionPool.getQueuedTransaction(id);
};

/**
 * @param {string} id
 * @return {function} Calls transactionPool.getMultisignatureTransaction
 */
Transactions.prototype.getMultisignatureTransaction = function (id) {
  return __private.transactionPool.getMultisignatureTransaction(id);
};

/**
 * Gets unconfirmed transactions based on limit and reverse option.
 * @param {boolean} reverse
 * @param {number} limit
 * @return {function} Calls transactionPool.getUnconfirmedTransactionList
 */
Transactions.prototype.getUnconfirmedTransactionList = function (reverse, limit) {
  return __private.transactionPool.getUnconfirmedTransactionList(reverse, limit);
};

/**
 * Gets queued transactions based on limit and reverse option.
 * @param {boolean} reverse
 * @param {number} limit
 * @return {function} Calls transactionPool.getQueuedTransactionList
 */
Transactions.prototype.getQueuedTransactionList = function (reverse, limit) {
  return __private.transactionPool.getQueuedTransactionList(reverse, limit);
};

/**
 * Gets multisignature transactions based on limit and reverse option.
 * @param {boolean} reverse
 * @param {number} limit
 * @return {function} Calls transactionPool.getQueuedTransactionList
 */
Transactions.prototype.getMultisignatureTransactionList = function (reverse, limit) {
  return __private.transactionPool.getMultisignatureTransactionList(reverse, limit);
};

/**
 * Gets unconfirmed, multisignature and queued transactions based on limit and reverse option.
 * @param {boolean} reverse
 * @param {number} limit
 * @return {function} Calls transactionPool.getMergedTransactionList
 */
Transactions.prototype.getMergedTransactionList = function (reverse, limit) {
  return __private.transactionPool.getMergedTransactionList(reverse, limit);
};

/**
 * Removes transaction from unconfirmed, queued and multisignature.
 * @param {string} id
 * @return {function} Calls transactionPool.removeUnconfirmedTransaction
 */
Transactions.prototype.removeUnconfirmedTransaction = function (id) {
  return __private.transactionPool.removeUnconfirmedTransaction(id);
};

/**
 * Checks kind of unconfirmed transaction and process it, resets queue
 * if limit reached.
 * @param {transaction} transaction
 * @param {Object} broadcast
 * @param {function} cb - Callback function.
 * @return {function} Calls transactionPool.processUnconfirmedTransaction
 */
Transactions.prototype.processUnconfirmedTransaction = function (transaction, broadcast, cb) {
  return __private.transactionPool.processUnconfirmedTransaction(transaction, broadcast, cb);
};

/**
 * Gets unconfirmed transactions list and applies unconfirmed transactions.
 * @param {function} cb - Callback function.
 * @return {function} Calls transactionPool.applyUnconfirmedList
 */
Transactions.prototype.applyUnconfirmedList = function (cb) {
  return __private.transactionPool.applyUnconfirmedList(cb);
};

/**
 * Applies unconfirmed list to unconfirmed Ids.
 * @param {string[]} ids
 * @param {function} cb - Callback function.
 * @return {function} Calls transactionPool.applyUnconfirmedIds
 */
Transactions.prototype.applyUnconfirmedIds = function (ids, cb) {
  return __private.transactionPool.applyUnconfirmedIds(ids, cb);
};

/**
 * Undoes unconfirmed list from queue.
 * @param {function} cb - Callback function.
 * @return {function} Calls transactionPool.undoUnconfirmedList
 */
Transactions.prototype.undoUnconfirmedList = function (cb) {
  return __private.transactionPool.undoUnconfirmedList(cb);
};

/**
 * Applies confirmed transaction.
 * @implements {logic.transaction.apply}
 * @param {transaction} transaction
 * @param {block} block
 * @param {account} sender
 * @param {function} cb - Callback function
 */
Transactions.prototype.apply = function (transaction, block, sender, cb) {
  library.logger.debug('Applying confirmed transaction', transaction.id);
  library.logic.transaction.apply(transaction, block, sender, cb);
};

/**
 * Undoes confirmed transaction.
 * @implements {logic.transaction.undo}
 * @param {transaction} transaction
 * @param {block} block
 * @param {account} sender
 * @param {function} cb - Callback function
 */
Transactions.prototype.undo = function (transaction, block, sender, cb) {
  library.logger.debug('Undoing confirmed transaction', transaction.id);
  library.logic.transaction.undo(transaction, block, sender, cb);
};

/**
 * Gets requester if requesterPublicKey and calls applyUnconfirmed.
 * @implements {modules.accounts.getAccount}
 * @implements {logic.transaction.applyUnconfirmed}
 * @param {transaction} transaction
 * @param {account} sender
 * @param {function} cb - Callback function
 * @return {setImmediateCallback} for errors
 */
Transactions.prototype.applyUnconfirmed = function (transaction, sender, cb) {
  library.logger.debug('Applying unconfirmed transaction', transaction.id);

  if (!sender && transaction.blockId !== library.genesisblock.block.id) {
    return setImmediate(cb, 'Invalid block id');
  } else {
    if (transaction.requesterPublicKey) {
      modules.accounts.getAccount({ publicKey: transaction.requesterPublicKey }, function (err, requester) {
        if (err) {
          return setImmediate(cb, err);
        }

        if (!requester) {
          return setImmediate(cb, 'Requester not found');
        }

        library.logic.transaction.applyUnconfirmed(transaction, sender, requester, cb);
      });
    } else {
      library.logic.transaction.applyUnconfirmed(transaction, sender, cb);
    }
  }
};

/**
 * Validates account and Undoes unconfirmed transaction.
 * @implements {modules.accounts.getAccount}
 * @implements {logic.transaction.undoUnconfirmed}
 * @param {transaction} transaction
 * @param {function} cb
 * @return {setImmediateCallback} For error
 */
Transactions.prototype.undoUnconfirmed = function (transaction, cb) {
  library.logger.debug('Undoing unconfirmed transaction', transaction.id);

  modules.accounts.getAccount({ publicKey: transaction.senderPublicKey }, function (err, sender) {
    if (err) {
      return setImmediate(cb, err);
    }
    library.logic.transaction.undoUnconfirmed(transaction, sender, cb);
  });
};

/**
 * Receives transactions
 * @param {transaction[]} transactions
 * @param {Object} broadcast
 * @param {function} cb - Callback function.
 * @return {function} Calls transactionPool.receiveTransactions
 */
Transactions.prototype.receiveTransactions = function (transactions, broadcast, cb) {
  return __private.transactionPool.receiveTransactions(transactions, broadcast, cb);
};

/**
 * Fills pool.
 * @param {function} cb - Callback function.
 * @return {function} Calls transactionPool.fillPool
 */
Transactions.prototype.fillPool = function (cb) {
  return __private.transactionPool.fillPool(cb);
};

/**
 * Calls helpers.sandbox.callMethod().
 * @implements module:helpers#callMethod
 * @param {function} call - Method to call.
 * @param {*} args - List of arguments.
 * @param {function} cb - Callback function.
 */
Transactions.prototype.sandboxApi = function (call, args, cb) {
  sandboxHelper.callMethod(shared, call, args, cb);
};

/**
 * Checks if `modules` is loaded.
 * @return {boolean} True if `modules` is loaded.
 */
Transactions.prototype.isLoaded = function () {
  return !!modules;
};

// Events
/**
 * Bounds scope to private transactionPool and modules
 * to private Transfer instance.
 * @implements module:transactions#Transfer~bind
 * @param {scope} scope - Loaded modules.
 */
Transactions.prototype.onBind = function (scope) {
  modules = {
    accounts: scope.accounts,
    transactions: scope.transactions,
    delegates: scope.delegates,
    chats: scope.chats,
    states: scope.states
  };

  __private.transactionPool.bind(
      scope.accounts,
      scope.transactions,
      scope.loader
  );
  __private.assetTypes[transactionTypes.SEND].bind(
      scope.accounts,
      scope.rounds
  );
};

// Shared API
/**
 * @todo implement API comments with apidoc.
 * @see {@link http://apidocjs.com/}
 */
Transactions.prototype.shared = {
  getTransactions: function (req, cb) {
    const requestBody = typeof req.body?.transaction === 'object' ?
      req.body.transaction : req.body;

    async.waterfall([
      function (waterCb) {
        var params = {};
        var pattern = /(and|or){1}:/i;

        // Filter out 'and:'/'or:' from params to perform schema validation
        _.each(requestBody, function (value, key) {
          var param = String(key).replace(pattern, '');
          // Dealing with array-like parameters (csv comma separated)
          if (_.includes(['senderIds', 'recipientIds', 'senderPublicKeys', 'recipientPublicKeys', 'types'], param)) {
            value = String(value).split(',');
            requestBody[key] = value;
          }
          params[param] = value;
        });

        if (params.types) {
          params.types = params.types.map((x) => parseInt(x));
        }

        library.schema.validate(params, schema.getTransactions, function (err) {
          if (err) {
            return setImmediate(waterCb, err[0].message);
          } else {
            return setImmediate(waterCb, null);
          }
        });
      },
      function (waterCb) {
        __private.list(requestBody, function (err, data) {
          if (err) {
            return setImmediate(waterCb, 'Failed to get transactions: ' + err);
          } else {
            return setImmediate(waterCb, null, { transactions: data.transactions, count: data.count });
          }
        });
      }
    ], function (err, res) {
      return setImmediate(cb, err, res);
    });
  },

  getTransaction: function (req, cb) {
    library.schema.validate(req.body, schema.getTransaction, function (err) {
      if (err) {
        return setImmediate(cb, err[0].message);
      }

      if (req.body.returnUnconfirmed) {
        const unconfirmedTransaction = __private.getUnconfirmedTransaction(req.body.id);

        if (unconfirmedTransaction) {
          const asset = req.body.returnAsset ? unconfirmedTransaction.asset : {};

          return {
            ...unconfirmedTransaction,
            asset
          };
        }
      }

      var method = 'getById';
      if (req.body.returnAsset) {
        method = 'getByIdFullAsset';
      }
      __private[method](req.body.id, function (err, transaction) {
        if (!transaction || err) {
          return setImmediate(cb, 'Transaction not found');
        }

        if (transaction.type === 3) {
          __private.getVotesById(transaction, function (err, transaction) {
            return setImmediate(cb, null, { transaction: transaction });
          });
        } else {
          return setImmediate(cb, null, { transaction: transaction });
        }
      });
    });
  },

  getTransactionsCount: function (req, cb) {
    library.db.query(sql.count).then(function (transactionsCount) {
      return setImmediate(cb, null, {
        confirmed: transactionsCount[0].count,
        multisignature: __private.transactionPool.countMultisignature(),
        unconfirmed: __private.transactionPool.countUnconfirmed(),
        queued: __private.transactionPool.countQueued()
      });
    }, function (err) {
      return setImmediate(cb, 'Unable to count transactions');
    });
  },

  getQueuedTransaction: function (req, cb) {
    return __private.getPooledTransaction('getQueuedTransaction', req, cb);
  },

  getQueuedTransactions: function (req, cb) {
    return __private.getPooledTransactions('getQueuedTransactionList', req, cb);
  },

  getMultisignatureTransaction: function (req, cb) {
    return __private.getPooledTransaction('getMultisignatureTransaction', req, cb);
  },

  getMultisignatureTransactions: function (req, cb) {
    return __private.getPooledTransactions('getMultisignatureTransactionList', req, cb);
  },

  getUnconfirmedTransaction: function (req, cb) {
    return __private.getPooledTransaction('getUnconfirmedTransaction', req, cb);
  },

  getUnconfirmedTransactions: function (req, cb) {
    return __private.getPooledTransactions('getUnconfirmedTransactionList', req, cb);
  },
  processTransactions: function (req, cb) {
    library.schema.validate(req.body.transaction, schema.processTransactions, function (err) {
      if (err) {
        return setImmediate(cb, err[0].message);
      }

      var query = { address: req.body.transaction.recipientId };

      library.balancesSequence.add(function (cb) {
        modules.accounts.getAccount(query, function (err, recipient) {
          if (err) {
            return setImmediate(cb, err);
          }

          var recipientId = recipient ? recipient.address : req.body.transaction.recipientId;

          if (!recipientId) {
            return setImmediate(cb, 'Invalid recipient');
          }

          if (req.body.multisigAccountPublicKey && req.body.multisigAccountPublicKey !== req.body.transaction.publicKey) {
            modules.accounts.getAccount({ publicKey: req.body.multisigAccountPublicKey }, function (err, account) {
              if (err) {
                return setImmediate(cb, err);
              }

              if (!account || !account.publicKey) {
                return setImmediate(cb, 'Multisignature account not found');
              }

              if (!Array.isArray(account.multisignatures)) {
                return setImmediate(cb, 'Account does not have multisignatures enabled');
              }

              if (account.multisignatures.indexOf(req.body.multisigAccountPublicKey.toString('hex')) < 0) {
                return setImmediate(cb, 'Account does not belong to multisignature group');
              }

              modules.accounts.getAccount({ publicKey: req.body.multisigAccountPublicKey }, function (err, requester) {
                if (err) {
                  return setImmediate(cb, err);
                }

                if (!requester || !requester.publicKey) {
                  return setImmediate(cb, 'Requester not found');
                }

                if (requester.secondSignature && !req.body.secondSecret) {
                  return setImmediate(cb, 'Missing requester second passphrase');
                }

                if (requester.publicKey === account.publicKey) {
                  return setImmediate(cb, 'Invalid requester public key');
                }

                var secondKeypair = null;

                if (requester.secondSignature) {
                  var secondHash = library.ed.createPassPhraseHash(req.body.secondSecret);
                  secondKeypair = library.ed.makeKeypair(secondHash);
                }

                var transaction;

                try {
                  transaction = library.logic.transaction.create({
                    type: transactionTypes.SEND,
                    amount: req.body.amount,
                    sender: account,
                    recipientId: recipientId,
                    keypair: null,
                    requester: null,
                    secondKeypair: secondKeypair
                  });
                } catch (e) {
                  return setImmediate(cb, e.toString());
                }

                modules.transactions.receiveTransactions([transaction], true, cb);
              });
            });
          } else {
            modules.accounts.setAccountAndGet({ publicKey: req.body.transaction.senderPublicKey }, function (err, account) {
              if (err) {
                return setImmediate(cb, err);
              }

              if (!account || !account.publicKey) {
                return setImmediate(cb, 'Account not found');
              }

              if (account.secondSignature && !req.body.secondSecret) {
                return setImmediate(cb, 'Missing second passphrase');
              }

              var secondKeypair = null;

              if (account.secondSignature) {
                var secondHash = library.ed.createPassPhraseHash(req.body.secondSecret);
                secondKeypair = library.ed.makeKeypair(secondHash);
              }

              var transaction;

              try {
                transaction = library.logic.transaction.publish(req.body.transaction);
              } catch (e) {
                return setImmediate(cb, e.toString());
              }

              modules.transactions.receiveTransactions([transaction], true, cb);
            });
          }
        });
      }, function (err, transaction) {
        if (err) {
          return setImmediate(cb, err);
        }

        return setImmediate(cb, null, { transactionId: transaction[0].id });
      });
    });
  },
  postTransactions: function (req, cb) {
    const tType = req.body.transaction !== undefined ? req.body.transaction.type : req.body.type;
    switch (tType) {
      case transactionTypes.CHAT_MESSAGE:
        modules.chats.internal.process(req, cb);
        break;
      case transactionTypes.STATE:
        modules.states.internal.store(req, cb);
        break;
    }
  }
};

// Export
module.exports = Transactions;
